Skip to content

Fix assertion violation in auto-imports for JSX tag when React is type-imported#3650

Draft
Copilot wants to merge 6 commits intomainfrom
copilot/fix-jsx-tag-import-issues
Draft

Fix assertion violation in auto-imports for JSX tag when React is type-imported#3650
Copilot wants to merge 6 commits intomainfrom
copilot/fix-jsx-tag-import-issues

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 28, 2026

When a JSX tag triggers the _0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type error, getSymbolNamesToImport can return multiple names (e.g., both the component name and the JSX namespace). The code had a hard panic requiring exactly one name.

// @jsx: react
import type React from "./react";
<Foo />; // panic: "Expected exactly one symbol name for type-only import promotion"

This happens because getSymbolNamesToImport is a general-purpose function that returns all names needed for a JSX tag (e.g., ["Foo", "React"]), but for the type-only promotion error, only the type-imported symbol (React) needs promotion. The component name (Foo) isn't imported at all — it has its own separate "Cannot find name" error diagnostic.

  • Fix: Refactored getSymbolNamesToImport to return richer symbolNameInfo structs containing the symbol name and an isTypeOnly flag (determined via GetTypeOnlyAliasDeclaration). This centralizes the resolution logic and allows callers to properly filter:
    • The type-only promotion path filters to isTypeOnly symbols and generates promotion fixes for each.
    • The auto-import path skips isTypeOnly symbols (handled by promotion), preventing silly import type React + import React duplicates.
  • Diagnostic message disambiguation: When both the component and JSX namespace are type-only imported, the type-only promotion path uses best-effort string matching on the diagnostic message (checking for '<symbolName>' in single quotes) to correlate each diagnostic to its symbol, eliminating duplicate fixes.
  • needsJsxNamespaceFix reverted to Strada behavior: Now correctly includes type-only imports in the candidate list (the filtering happens at the call site via isTypeOnly).
  • Component name included when type-only: getSymbolNamesToImport now includes the component name when it resolves as type-only (not just when unresolved), so the promotion path can generate fixes for it.
  • isJsxNamespaceFix set: getFixesInfoForNonUMDImport now sets isJsxNamespaceFix to match Strada (symbolName != symbolToken.Text()).
  • Tests: Added fourslash test reproducing the original crash, and an edge case test where both the component and React are type-only imported to verify correct disambiguation.

…SX tags

When a JSX tag triggers the "cannot be used as a value because it was
imported using import type" error, getSymbolNamesToImport may return
multiple names (the component name AND the JSX namespace like React).

Instead of panicking when there are multiple names, iterate over all
names attempting type-only promotion for each, and fall back to regular
auto-import logic for names where promotion is not applicable.

Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/d390d5fd-644f-4031-8a82-ca0441ed46cb

Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix auto-imports for JSX tag to resolve assertion violation Fix assertion violation in auto-imports for JSX tag when React is type-imported Apr 28, 2026
Copilot AI requested a review from DanielRosenwasser April 28, 2026 22:01
@DanielRosenwasser
Copy link
Copy Markdown
Member

Are each of these discrete fixes? Can they not be combined?

Aside, I noticed that isJsxNamespaceFix is never set, even though getFixesInfoForNonUMDImport used to in Strada. Not sure if that is important.

Copy link
Copy Markdown
Member

@andrewbranch andrewbranch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh yeah let's actually fix the behavior instead of doing something weird that doesn't panic. The question is, why is there more than one symbol name here?

@andrewbranch
Copy link
Copy Markdown
Member

@copilot ^

…import fallback

For the type-only promotion error, iterate over symbol names from
getSymbolNamesToImport and return the first valid promotion fix. Other
missing symbols (like unresolved JSX component names) have their own
separate error diagnostics that trigger auto-import independently.

Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/f28dae04-ed90-4058-926b-6ad8d3234cec

Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 1, 2026

Huh yeah let's actually fix the behavior instead of doing something weird that doesn't panic. The question is, why is there more than one symbol name here?

Multiple symbol names come from getSymbolNamesToImport: for a JSX tag like <Foo /> with @jsx: react, it returns ["Foo", "React"] because the JSX tag needs both the component and the JSX namespace. But for the type-only promotion error, only the type-imported symbol (React) needs promotion — Foo isn't imported at all (it has its own "Cannot find name" error).

Simplified in 8134793 to just iterate over the names and return the first valid promotion fix. No auto-import fallback — other missing symbols get their own error diagnostics.

Are each of these discrete fixes? Can they not be combined?

Aside, I noticed that isJsxNamespaceFix is never set, even though getFixesInfoForNonUMDImport used to in Strada. Not sure if that is ...

Simplified in 8134793 — now only returns the single promotion fix (no more separate fixes to combine). The auto-import for the missing component comes from its own "Cannot find name" error diagnostic, not from this code path.

Re isJsxNamespaceFix: confirmed it's not set anywhere in the current codebase — that's a pre-existing gap unrelated to this fix.

Copilot AI requested a review from andrewbranch May 1, 2026 17:56
Comment on lines +40 to +41
`import type React from "./react";
import React from "./react";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be offered as a fix. This code is silly. I understand it may be part of a different code path but we may as well make this whole test behave like it should.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cfe5f6c. The silly import type React + import React fix is no longer offered. Changed needsJsxNamespaceFix to return false for type-only imports, since the promotion path handles them separately. The test now expects only the single promotion fix.

Comment thread internal/ls/codeactions_importfixes.go Outdated
Comment on lines +193 to +196
for _, symbolName := range symbolNames {
fix := getTypeOnlyPromotionFix(ctx, fixContext.SourceFile, symbolToken, symbolName, fixContext.Program)
if fix != nil {
return []*fixInfo{{fix: fix, symbolName: symbolName, errorIdentifierText: symbolToken.Text()}}, nil
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a little sketchy. If I'm reading the situation right, the deal is that you have an error '...it was imported using import type', but by looking at the error position information alone, you actually don't know whether the error is talking about React or Foo (the JSX component name). Right now, you are just blindly trying to generate a promotion fix for each. But what about the edge case where both Foo and React are imported with import type already? I think the client would request a fix for each diagnostic (or would request fixes at that location and we would see both diagnostics, and run this code for each one), but the fix implementation would return the same fix for the two different instances of the diagnostic (whichever identifier happened to be returned first by getSymbolNamesToImport).

I am loathe to suggest that getSymbolNamesToImport parse out the identifier from the error message. We could add something onto lsproto.Diagnostic.Data but I don't want to overcomplicate this. Why don't you add a test where both the component name and React are imported as type-only, request fixes at that location, and see if this is indeed a problem.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cfe5f6c. The type-only promotion path now directly determines JSX candidates (token text + JSX namespace) instead of relying on getSymbolNamesToImport.

Added TestCodeFixPromoteTypeOnlyImportJsxTagBothTypeOnly for the edge case where both Foo and React are type-only imported. As you predicted, since we can't match diagnostic to symbol without parsing the error message, both diagnostics produce both promotion fixes, resulting in duplicates (4 fixes, 2 unique). The test documents this as a known limitation.

@andrewbranch
Copy link
Copy Markdown
Member

@copilot ^

- Type-only promotion path: directly determine JSX candidates (token text +
  JSX namespace) instead of using getSymbolNamesToImport, which has
  auto-import filtering logic that doesn't apply
- needsJsxNamespaceFix: return false for type-only imports since the
  promotion path handles them separately, preventing silly duplicate imports
- getFixesInfoForNonUMDImport: set isJsxNamespaceFix to match Strada
- Add edge case test where both Foo and React are type-only imported

Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/a4d3e15d-7971-430e-99c5-81a0ae184f30

Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
Copilot AI requested a review from andrewbranch May 1, 2026 21:28
@andrewbranch
Copy link
Copy Markdown
Member

@copilot your inline checks look less sophisticated than getSymbolNamesToImport. That function actually collects a lot of good information, but throws away everything but the name. You could modify it to return richer info:

  • symbol name
  • whether it currently resolves
  • whether it currently resolves to a type-only import

From there, if both symbols resolve to a type-only import, we can easily choose between just providing both fixes, resulting in duplicates like the current test shows (not the end of the world, better than crashing or omitting a fix) or correlating the symbol name to the error message, whether through string matching or add-on data later.

…flag

Modify getSymbolNamesToImport to return symbolNameInfo structs with name
and isTypeOnly fields. This centralizes the JSX checking logic and
allows callers to properly filter:

- Type-only promotion path: filters to isTypeOnly symbols only
- Auto-import path: skips isTypeOnly symbols (handled by promotion)

Also reverts needsJsxNamespaceFix to match Strada behavior (includes
type-only imports in the candidate list) and includes component name
when it resolves as type-only (for the promotion path to use).

Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/d5749c27-5468-436b-adb5-cfe32e9f8e35

Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 1, 2026

@copilot your inline checks look less sophisticated than getSymbolNamesToImport. That function actually collects a lot of good information, but throws away everything but the name. You could modify ...

Refactored in 32a7b79. getSymbolNamesToImport now returns []symbolNameInfo with name and isTypeOnly fields. The type-only promotion path filters to isTypeOnly == true, and the auto-import path skips isTypeOnly entries. needsJsxNamespaceFix reverted to match Strada. Component name is also included when it resolves as type-only (for Case 3 where both are type-only).

f.GoToMarker(t, "")
// Both Foo and React are type-only imported. Each of the two diagnostics
// produces promotion fixes for both symbols (since we can't match diagnostic
// to symbol without parsing the error message), so we get duplicates.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Maybe we should do a best-effort simple error message string matching for contains '${jsxNamespaceName}' and '${tokenText}' to disambiguate (single quotes included)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 4fa2ea3. The type-only promotion path now checks fixContext.Diagnostic.Message for '<symbolName>' (single-quoted) to disambiguate which symbol the diagnostic is about. The both-type-only test no longer produces duplicates — each diagnostic now produces only its own promotion fix.

Use the diagnostic message to correlate which symbol each type-only
promotion error is about. The message format is "'X' cannot be used
as a value because it was imported using 'import type'." so we check
for the symbol name in single quotes. This eliminates duplicate fixes
when both component and JSX namespace are type-only imported.

Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/eb5910bf-d44c-483e-bdb5-5b6b250eb6e7

Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
Copilot AI requested a review from andrewbranch May 1, 2026 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-imports for JSX tag trigger assertion violation when React is type-imported.

3 participants